﻿#region BSD 3-Clause License
/* Copyright (c) 2012, Intel Corporation
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *    
 * 3. Neither the name of Intel Corporation nor the names of its contributors may
 *    be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#endregion

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;

namespace SpartanUnit
{
    public abstract class TestSuite : ITestFixture, ITestSuite
    {
        private static ConcurrentQueue<TestException> _testExceptionsQueue = new ConcurrentQueue<TestException>();
        private static TestEventBroadcaster _testEventsBroadcaster = new TestEventBroadcaster();

        private ICollection<ITestSuite> _testSuites = new Collection<ITestSuite>();
        private bool _mayExecuteTestSuitesInParallel = true;
        private bool _mayExecuteTestsInParallel = false;
        private bool _mayContinueWhenTestFails = true;
        private ITestFixture _testFixture = null;

        protected TestSuite()
        {
            _testFixture = this;
        }

        protected TestSuite(ITestFixture testFixture)
            : this()
        {
            _testFixture = testFixture;
        }

        public ITestFixture TestFixture
        {
            get
            {
                return _testFixture;
            }
        }

        public ICollection<ITestSuite> TestSuites
        {
            get
            {
                return _testSuites;
            }
        }

        public static TestEventBroadcaster Broadcaster
        {
            get
            {
                return _testEventsBroadcaster;
            }
        }

        public static int TestExceptionCount
        {
            get
            {
                return _testExceptionsQueue.Count;
            }
        }

        public virtual void Setup()
        {
        }

        public virtual void Teardown()
        {
        }

        public void Execute()
        {
            ExecuteTestSuites();
            ExecuteTestCases();
        }

        private void ExecuteTestSuites()
        {
            if (MustExecuteTestSuitesInSeries)
            {
                ExecuteTestSuitesInSeries();
            }
            else
            {
                ExecuteTestSuitesInParallel();
            }
        }

        private void ExecuteTestCases()
        {
            if (MustExecuteTestCasesInSeries)
            {
                ExecuteTestCasesInSeries();
            }
            else
            {
                ExecuteTestCasesInParallel();
            }
        }

        private void ExecuteTestSuitesInSeries()
        {
            foreach (ITestSuite testSuite in this.TestSuites)
            {
                try
                {
                    testSuite.Execute();
                }
                catch (TestException testException)
                {
                    EnqueueTestException(testException);
                }
                catch (AggregateException aggregateException)
                {
                    EnqueueTestExceptions(aggregateException);
                }
                if (TestExceptionCount > 0 && !MayContinueWhenTestFails)
                {
                    break;
                }
            }
        }

        private void ExecuteTestCasesInSeries()
        {
            foreach (MethodInfo method in this.GetType().GetMethods())
            {
                try
                {
                    TestCaseAttribute testCaseAttribute = (TestCaseAttribute)Attribute.GetCustomAttribute
                        (method, typeof(TestCaseAttribute));

                    if (testCaseAttribute != null)
                    {
                        ExecuteTestCase(method);
                    }
                }
                catch (TestException testException)
                {
                    EnqueueTestException(testException);
                }
                if (TestExceptionCount > 0 && !MayContinueWhenTestFails)
                {
                    break;
                }
            }
        }

        private void ExecuteTestSuitesInParallel()
        {
            Debug.Assert(MayExecuteTestSuitesInParallel);

            try
            {
                Parallel.ForEach(this.TestSuites, testSuite =>
                {
                    testSuite.Execute();
                });
            }
            catch (TestException testException)
            {
                EnqueueTestException(testException);
            }
            catch (AggregateException aggregateException)
            {
                EnqueueTestExceptions(aggregateException);
            }
        }

        private void ExecuteTestCasesInParallel()
        {
            Debug.Assert(MayExecuteTestCasesInParallel);

            try
            {
                Parallel.ForEach(this.GetType().GetMethods(), method =>
                {
                    TestCaseAttribute testCaseAttribute = (TestCaseAttribute)Attribute.GetCustomAttribute
                        (method, typeof(TestCaseAttribute));

                    if (testCaseAttribute != null)
                    {
                        ExecuteTestCase(method);
                    }
                });
            }
            catch (TestException testException)
            {
                EnqueueTestException(testException);
            }
            catch (AggregateException aggregateException)
            {
                EnqueueTestExceptions(aggregateException);
            }
        }

        /// <remarks>
        /// As a result of using reflection, when a test fails Visual Studio might "break" due
        /// to an unhandled user exception. The discussion at the following URL and the excerpt
        /// below it explains why and what you can do about it. In a sense, this is not terrible
        /// behavior in this particular case, since users might appreciate being forced to
        /// acknowledge test failures.
        /// 
        /// http://stackoverflow.com/questions/2658908/why-is-targetinvocationexception-treated-as-uncaught-by-the-ide
        ///
        /// QUOTE (slightly modified to correspond to what I see in my version of VS 2010):
        ///
        /// As the help file here states: http://msdn.microsoft.com/en-us/library/038tzxdw.aspx
        /// The Debug -> Exceptions dialog shows an additional column (Break when an exception is
        /// User-unhandled) when "Enable Just My Code" is on. Essentially this means that whenever
        /// the exception is leaving the boundary of your code (and in this case, it falls through
        /// down to the .NET framework reflection code), VS breaks because it thinks that the
        /// exception has left the user code. It doesn't know that it will return into the user
        /// code later in the stack.
        ///
        /// So there are [a few] workarounds:
        ///
        /// Disable Just My Code in Tools -> Options -> Debugging -> General or
        ///
        /// [Uncheck the CLR Exceptions] check box from the User-unhandled [column] in the
        /// Debug -> Exceptions dialog.
        ///
        /// [Or tollerate the break in execution and simply press F5 to continue.]
        /// </remarks>
        private void ExecuteTestCase(MethodInfo testCaseMethodInfo)
        {
            Debug.Assert(testCaseMethodInfo != null);
            TestEventArgs testEventArgs = new TestEventArgs(this, testCaseMethodInfo);

            // Pre-Test
            testEventArgs.TestExecutionRecord.LogEventStartedNow();
            TestSuite.Broadcaster.SignalTestStarted(testEventArgs);
            this.TestFixture.Setup();

            try
            {
                // Execute Test
                testCaseMethodInfo.Invoke(this, null);
                testEventArgs.TestExecutionRecord.LogTestPassed();
                TestSuite.Broadcaster.SignalTestPassed(testEventArgs);
            }
            catch (TargetInvocationException targetInvocationException)
            {
                // If Test Fails
                AssertException assertException = (AssertException)targetInvocationException.InnerException;
                if (assertException != null)
                {
                    TestException testException = new TestException("Test Failed", assertException);
                    testException.Source = this.ToString() + "." + testCaseMethodInfo.Name;
                    testEventArgs.TestExecutionRecord.LogTestFailed(testException);
                    TestSuite.Broadcaster.SignalTestFailed(testEventArgs);
                    throw testException;
                }
                else
                {
                    throw targetInvocationException.InnerException;
                }
            }
            finally
            {
                // Post-Test
                this.TestFixture.Teardown();
                testEventArgs.TestExecutionRecord.LogEventFinishedNow();
                TestSuite.Broadcaster.SignalTestFinished(testEventArgs);
            }
        }

        private static void EnqueueTestException(TestException testException)
        {
            _testExceptionsQueue.Enqueue(testException);
        }

        private static void EnqueueTestExceptions(AggregateException aggregateException)
        {
            if (aggregateException != null)
            {
                aggregateException.Flatten().Handle(exception =>
                {
                    TestException testException = exception as TestException;

                    if (testException != null)
                    {
                        _testExceptionsQueue.Enqueue(testException);
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                });
            }
        }

        public bool MayContinueWhenTestFails
        {
            get
            {
                return _mayContinueWhenTestFails;
            }
            set
            {
                _mayContinueWhenTestFails = value;
            }
        }

        public bool MayExecuteTestSuitesInParallel
        {
            get
            {
                return _mayExecuteTestSuitesInParallel;
            }
            set
            {
                _mayExecuteTestSuitesInParallel = value;
            }
        }

        public bool MustExecuteTestSuitesInSeries
        {
            get
            {
                return !_mayExecuteTestSuitesInParallel;
            }
            set
            {
                MayExecuteTestSuitesInParallel = !value;
            }
        }

        public bool MayExecuteTestCasesInParallel
        {
            get
            {
                return _mayExecuteTestsInParallel;
            }
            set
            {
                _mayExecuteTestsInParallel = value;
            }
        }

        public bool MustExecuteTestCasesInSeries
        {
            get
            {
                return !_mayExecuteTestsInParallel;
            }
            set
            {
                _mayExecuteTestsInParallel = !value;
            }
        }

    }
}
